iOS HTTPS相关整理

HTTPS

HTTPS与HTTP没有任何的区别,HTTPS就是将HTTP协议数据包放到SSL/TSL层加密后,在TCP/IP层组成IP数据报去传输,以此保证传输数据的安全;而对于接收端,在SSL/TSL将接收的数据包解密之后,将数据传给HTTP协议层,就是普通的HTTP数据。HTTP和SSL/TSL都处于OSI模型的应用层。HTTPS连接建立过程大致是,客户端和服务端建立一个连接,服务端返回一个证书,客户端里存有各个受信任的证书机构根证书,用这些根证书对服务端返回的证书进行验证,经验证如果证书是可信任的,就生成一个pre-master secret,用这个证书的公钥加密后发送给服务端,服务端用私钥解密后得到pre-master secret,再根据某种算法生成master secret,客户端也同样根据这种算法从pre-master secret生成master secret,随后双方的通信都用这个master secret对传输数据进行加密解密。

参考阅读
HTTPS那些事
SSL/TLS协议运行机制的概述
图解SSL/TLS协议

SSL证书,有以下几种方式:

  1. 使用正规的商用授权证书,需要花钱的,据说也不算贵。四五十美金一年(自己没用过)。如果有这样的证书,AFNetworking不用任何额外的设置,只是在POST或者GET是使用的URL变成https开头的就可以了。
  2. 使用自己生成SSL证书:也就是使用无效证书。这种方式也比较普遍,尤其对中小企业和个人开发来讲,免费总是好的,而且灵活自如。随时可以更换证书。对于这种方式,AFNetworking也是支持的,但需要额外的几行配置代码才行。同时,这种方式有两种不同的连接方式:
    2.1:证书不保存在手机客户端,客户端接收所有无效证书。
    2.2:客户端只接受来自特定服务器端的证书。

SSL Pinning

可以理解为证书绑定,是指客户端直接保存服务端的证书,建立https连接时直接对比服务端返回的和客户端保存的两个证书是否一样,一样就表明证书是真的,不再去系统的信任证书机构里寻找验证。这适用于非浏览器应用,因为浏览器跟很多未知服务端打交道,无法把每个服务端的证书都保存到本地,但CS架构的像手机APP事先已经知道要进行通信的服务端,可以直接在客户端保存这个服务端的证书用于校验。
为什么直接对比就能保证证书没问题?如果中间人从客户端取出证书,再伪装成服务端跟其他客户端通信,它发送给客户端的这个证书不就能通过验证吗?确实可以通过验证,但后续的流程走不下去,因为下一步客户端会用证书里的公钥加密,中间人没有这个证书的私钥就解不出内容,也就截获不到数据,这个证书的私钥只有真正的服务端有,中间人伪造证书主要伪造的是公钥。
为什么要用SSL Pinning?正常的验证方式不够吗?如果服务端的证书是从受信任的的CA机构颁发的,验证是没问题的,但CA机构颁发证书比较昂贵,小企业或个人用户可能会选择自己颁发证书,这样就无法通过系统受信任的CA机构列表验证这个证书的真伪了,所以需要SSL Pinning这样的方式去验证.

NSURLConnection, NSURLSession实现支持HTTPS

先说下最常用的NSURLConnection支持HTTPS的实现(NSURLSession的实现方法类似,只是要求授权证明的回调不一样而已)后面讲说说怎样使用AFNetworking支持HTTPS

验证流程如下:

  1. 第一步,先获取需要验证的信任对象(Trust Object)。这个Trust Object在不同的应用场景下获取的方式都不一样,对于NSURLConnection来说,是从delegate方法-connection:willSendRequestForAuthenticationChallenge:回调回来的参数challenge中获取([challenge.protectionSpace serverTrust])。

  2. 使用系统默认验证方式验证Trust Object。SecTrustEvaluate会根据Trust Object的验证策略,一级一级往上,验证证书链上每一级数字签名的有效性(上一部分有讲解),从而评估证书的有效性。

  3. 如第二步验证通过了,一般的安全要求下,就可以直接验证通过,进入到下一步:使用Trust Object生成一份凭证([NSURLCredential credentialForTrust:serverTrust]),传入challenge的sender中([challenge.sender useCredential:cred forAuthenticationChallenge:challenge])处理,建立连接。

  4. 假如有更强的安全要求,可以继续对Trust Object进行更严格的验证。常用的方式是在本地导入证书,验证Trust Object与导入的证书是否匹配。更多的方法可以查看Enforcing Stricter Server Trust Evaluation,这一部分在讲解AFNetworking源码中会讲解到。

  5. 假如验证失败,取消此次Challenge-Response Authentication验证流程,拒绝连接请求。

如果采用自建证书则会跳过第二步,使用第三部进行验证,因为自建证书的根CA的数字签名未在操作系统的信任列表中

NSURLConnection中的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//使用NSURLConnection支持HTTPS的实现
// Now start the connection
NSURL * httpsURL = [NSURL URLWithString:@"https://www.google.com"];
self.connection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:httpsURL] delegate:self];
//回调
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
//1)获取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//2)SecTrustEvaluate对trust进行验证
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess &&
(result == kSecTrustResultProceed ||
result == kSecTrustResultUnspecified)) {
//3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
} else {
//5)验证失败,取消这次验证流程
[challenge.sender cancelAuthenticationChallenge:challenge];
}
}

上面是代码是通过系统默认验证流程来验证证书的。假如我们是自建证书的呢?这样Trust Object里面服务器的证书因为不是可信任的CA签发的,所以直接使用SecTrustEvaluate进行验证是不会成功。又或者,即使服务器返回的证书是信任CA签发的,又如何确定这证书就是我们想要的特定证书?这就需要先在本地导入证书,设置成需要验证的Anchor Certificate(就是根证书),再调用SecTrustEvaluate来验证。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//导入证书
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"你的证书" ofType:@"cer"];
NSData * cerData = [NSData dataWithContentsOfFile:cerPath];
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(cerData));
self.trustedCertificates = @[CFBridgingRelease(certificate)];
//回调
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
//1)获取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//注意:这里将之前导入的证书设置成下面验证的Trust Object的anchor certificate
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);
//2)SecTrustEvaluate会查找前面SecTrustSetAnchorCertificates设置的证书或者系统默认提供的证书,对trust进行验证
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess &&
(result == kSecTrustResultProceed ||
result == kSecTrustResultUnspecified)) {
//3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
} else {
//5)验证失败,取消这次验证流程
[challenge.sender cancelAuthenticationChallenge:challenge];
}
}

AFNetworking支持HTTPS

AFNetworking已经将上面的逻辑代码封装好,甚至更完善,在AFSecurityPolicy文件中

AFSecurityPolicy

NSURLConnection已经封装了https连接的建立、数据的加密解密功能,我们直接使用NSURLConnection是可以访问https网站的,但NSURLConnection并没有验证证书是否合法,无法避免中间人攻击。要做到真正安全通讯,需要我们手动去验证服务端返回的证书,AFSecurityPolicy封装了证书验证的过程,让用户可以轻易使用,除了去系统信任CA机构列表验证,还支持SSL Pinning方式的验证。

AFSecurityPolicy分三种验证模式:

  1. AFSSLPinningModeNone
    这个模式表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。

  2. AFSSLPinningModeCertificate
    这个模式表示用证书绑定方式验证证书,需要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名/有效期等信息,第二步是对比服务端返回的证书跟客户端返回的是否一致。
    这里还没弄明白第一步的验证是怎么进行的,代码上跟去系统信任机构列表里验证一样调用了SecTrustEvaluate,只是这里的列表换成了客户端保存的那些证书列表。若要验证这个,是否应该把服务端证书的颁发机构根证书也放到客户端里?

  3. AFSSLPinningModePublicKey
    这个模式同样是用证书绑定方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    NSURL * url = [NSURL URLWithString:@"https://www.google.com"];
    AFHTTPRequestOperationManager * requestOperationManager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:url];
    dispatch_queue_t requestQueue = dispatch_create_serial_queue_for_name("kRequestCompletionQueue");
    requestOperationManager.completionQueue = requestQueue;
    //验证模式 AFSSLPinningModeCertificate
    AFSecurityPolicy * securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    //allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
    //如果是需要验证自建证书,需要设置为YES
    securityPolicy.allowInvalidCertificates = YES;
    //validatesDomainName 是否需要验证域名,默认为YES;
    //假如证书的域名与你请求的域名不一致,需把该项设置为NO
    //主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
    securityPolicy.validatesDomainName = NO;
    //validatesCertificateChain 是否验证整个证书链,默认为YES
    //设置为YES,会将服务器返回的Trust Object上的证书链与本地导入的证书进行对比,这就意味着,假如你的证书链是这样的:
    //GeoTrust Global CA
    // Google Internet Authority G2
    // *.google.com
    //那么,除了导入*.google.com之外,还需要导入证书链上所有的CA证书(GeoTrust Global CA, Google Internet Authority G2);
    //如是自建证书的时候,可以设置为YES,增强安全性;假如是信任的CA所签发的证书,则建议关闭该验证;
    securityPolicy.validatesCertificateChain = NO;
    requestOperationManager.securityPolicy = securityPolicy;

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//把服务端证书(需要转换成cer格式)放到APP项目资源里,AFSecurityPolicy会自动寻找根目录下所有cer文件
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
// 添加security policy
// 注意:PinningMode必需是使用Public Key。如果是None,将接收所有自定义的无效证书,会产生安全漏洞
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
// 设置可以接收无效的证书
[securityPolicy setAllowInvalidCertificates:YES];
// 设置不检测整个证书链,只要其中一个正确即可
[securityPolicy setValidatesCertificateChain:NO];
manager.securityPolicy = securityPolicy;
// 正常使用GET和POST,URL里是https
[manager GET:@"https://example.com/resources.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"JSON: %@", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"Error: %@", error);
}];

参考 AFNetworking2.0源码解
http://http://blog.cnbang.net/tech
参考 iOS安全系列之一:HTTPS
http://oncenote.com/2014/10/21/Security-1-HTTPS/
参考 iOS https(SSL/TLS)数据捕获
http://www.h4ck.org.cn/2013/08/ios-httpsssltls%E6%95%B0%E6%8D%AE%E6%8D%95%E8%8E%B7/